/*
 * Copyright 2002-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.transaction.support;

/**
 * {@link TransactionSynchronization} implementation that manages a
 * {@link ResourceHolder} bound through {@link TransactionSynchronizationManager}.
 *
 * @author Juergen Hoeller
 * @since 2.5.5
 */
public abstract class ResourceHolderSynchronization<H extends ResourceHolder, K>
        implements TransactionSynchronization {

    private final H resourceHolder;

    private final K resourceKey;

    private volatile boolean holderActive = true;


    /**
     * Create a new ResourceHolderSynchronization for the given holder.
     *
     * @param resourceHolder the ResourceHolder to manage
     * @param resourceKey the key to bind the ResourceHolder for
     * @see TransactionSynchronizationManager#bindResource
     */
    public ResourceHolderSynchronization(H resourceHolder, K resourceKey) {
        this.resourceHolder = resourceHolder;
        this.resourceKey = resourceKey;
    }


    @Override
    public void suspend() {
        if (this.holderActive) {
            TransactionSynchronizationManager.unbindResource(this.resourceKey);
        }
    }

    @Override
    public void resume() {
        if (this.holderActive) {
            TransactionSynchronizationManager.bindResource(this.resourceKey, this.resourceHolder);
        }
    }

    @Override
    public void flush() {
        flushResource(this.resourceHolder);
    }

    @Override
    public void beforeCommit(boolean readOnly) {
    }

    @Override
    public void beforeCompletion() {
        if (shouldUnbindAtCompletion()) {
            TransactionSynchronizationManager.unbindResource(this.resourceKey);
            this.holderActive = false;
            if (shouldReleaseBeforeCompletion()) {
                releaseResource(this.resourceHolder, this.resourceKey);
            }
        }
    }

    @Override
    public void afterCommit() {
        if (!shouldReleaseBeforeCompletion()) {
            processResourceAfterCommit(this.resourceHolder);
        }
    }

    @Override
    public void afterCompletion(int status) {
        if (shouldUnbindAtCompletion()) {
            boolean releaseNecessary = false;
            if (this.holderActive) {
                // The thread-bound resource holder might not be available anymore,
                // since afterCompletion might get called from a different thread.
                this.holderActive = false;
                TransactionSynchronizationManager.unbindResourceIfPossible(this.resourceKey);
                this.resourceHolder.unbound();
                releaseNecessary = true;
            } else {
                releaseNecessary = shouldReleaseAfterCompletion(this.resourceHolder);
            }
            if (releaseNecessary) {
                releaseResource(this.resourceHolder, this.resourceKey);
            }
        } else {
            // Probably a pre-bound resource...
            cleanupResource(this.resourceHolder, this.resourceKey, (status == STATUS_COMMITTED));
        }
        this.resourceHolder.reset();
    }


    /**
     * Return whether this holder should be unbound at completion
     * (or should rather be left bound to the thread after the transaction).
     * <p>The default implementation returns {@code true}.
     */
    protected boolean shouldUnbindAtCompletion() {
        return true;
    }

    /**
     * Return whether this holder's resource should be released before
     * transaction completion ({@code true}) or rather after
     * transaction completion ({@code false}).
     * <p>Note that resources will only be released when they are
     * unbound from the thread ({@link #shouldUnbindAtCompletion()}).
     * <p>The default implementation returns {@code true}.
     *
     * @see #releaseResource
     */
    protected boolean shouldReleaseBeforeCompletion() {
        return true;
    }

    /**
     * Return whether this holder's resource should be released after
     * transaction completion ({@code true}).
     * <p>The default implementation returns {@code !shouldReleaseBeforeCompletion()},
     * releasing after completion if no attempt was made before completion.
     *
     * @see #releaseResource
     */
    protected boolean shouldReleaseAfterCompletion(H resourceHolder) {
        return !shouldReleaseBeforeCompletion();
    }

    /**
     * Flush callback for the given resource holder.
     *
     * @param resourceHolder the resource holder to flush
     */
    protected void flushResource(H resourceHolder) {
    }

    /**
     * After-commit callback for the given resource holder.
     * Only called when the resource hasn't been released yet
     * ({@link #shouldReleaseBeforeCompletion()}).
     *
     * @param resourceHolder the resource holder to process
     */
    protected void processResourceAfterCommit(H resourceHolder) {
    }

    /**
     * Release the given resource (after it has been unbound from the thread).
     *
     * @param resourceHolder the resource holder to process
     * @param resourceKey the key that the ResourceHolder was bound for
     */
    protected void releaseResource(H resourceHolder, K resourceKey) {
    }

    /**
     * Perform a cleanup on the given resource (which is left bound to the thread).
     *
     * @param resourceHolder the resource holder to process
     * @param resourceKey the key that the ResourceHolder was bound for
     * @param committed whether the transaction has committed ({@code true})
     * or rolled back ({@code false})
     */
    protected void cleanupResource(H resourceHolder, K resourceKey, boolean committed) {
    }

}
